package com.sd365.gateway.authorization.service.impl;


import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSONObject;
import com.sd365.common.constant.Global;
import com.sd365.common.util.BeanUtil;
import com.sd365.gateway.authorization.constant.BusinessResultConsts;
import com.sd365.gateway.authorization.dao.mapper.ResourceMapper;
import com.sd365.gateway.authorization.dao.mapper.RoleResourceMapper;
import com.sd365.permission.centre.entity.RoleResource;
import com.sd365.gateway.authorization.service.AuthorizationService;
import com.sd365.gateway.authorization.service.RenewTokenService;
import com.sd365.gateway.authorization.service.remote.UserService;
import com.sd365.permission.centre.entity.Resource;
import com.sd365.permission.centre.pojo.vo.ResourceVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import tk.mybatis.mapper.entity.Example;


import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.sd365.common.util.TokenUtil.parseTokenPayload;
import static java.util.regex.Pattern.compile;

/**
 * @version 1.0.0
 * @Class AuthorizationServiceImpl
 * @Description 1 从架构上重构了认证service 将 token续约分离出去定义在 RenewTokenService
 * 2 依据用户中心重构的 redis 以roleId 为key 构建对应的资源在hashmap的逻辑，重构了
 * 鉴权的角色对应的资源取得以及判定
 * @Author Administrator
 * @Date 2023-04-26  22:14
 */
@Slf4j
@RefreshScope
@Service
public class AuthorizationServiceImpl implements AuthorizationService {

    /**
     * 注意配置文件增加配置 调整该配置到 app。secret下
     */
    @Value("${app.secret.hasOpenAuthorization}")
    private Boolean hasOpenAuthorization;
    /**
     * Jwt Token签名秘钥
     */
    @Value("${app.secret.token.key}")
    private String secret = "$6$bosssoft$";
    /**
     *  角色id 作为标示hashmap的key
     */
//    private static final String HASH_ROLE_ID_KEY="uc:cache:hash:role:id:";
    /**
     *  resource id作为缓存key
     */
//    private static final String RESOURCE_ID_KEY="uc:cache:resource:id:";

    /**
     * 匹配请求URL的正则表达式
     */
    private static final String AUTHOR_REQUEST_URL_EXPR = "^https?:\\/\\/(?:[0-9a-zA-Z:\\.]*)([^\\?]*)";

    /**
     * 如果redis没取到则使用 该mapper取数据库
     */
    @Autowired
    private ResourceMapper resourceMapper;
    /**
     * abel.zhan 增加 用于找到角色对应的资源列表
     */
    @Autowired
    private RoleResourceMapper roleResourceMapper;

    /**
     * 该redisTemplate 支持json序列化
     */
    @javax.annotation.Resource(name = "roleResourceRedisTemplate")
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 引入续约业务类做token状态判断
     */
    @Autowired
    private RenewTokenService renewTokenService;
    /**
     * 调用用户中心的UserApi的 getUserResourceVO接口使用
     * 此为FeignClient对象
     */
    private UserService userService;

    @Override
    public Boolean commonResource(String url) {
        final Pattern compile = compile(AUTHOR_REQUEST_URL_EXPR);
        Matcher matcher = compile.matcher(url);
        if (matcher.find()) {
            url = matcher.group(1);
        }
        List<Resource> commonResources = getCommonResource();
        log.info("通用资源为空，将正常访问资源");
        if (!CollectionUtils.isEmpty(commonResources)) {
            for (Resource resource : commonResources) {
                if (!StringUtils.isEmpty(resource.getApi())) {
                    final Pattern pattern = compile(String.format("%s$", resource.getApi()));
                    final Matcher matcherApi = pattern.matcher(url);
                    if (matcherApi.find()) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * 获取通用资源，避免网关鉴权访问数据库而影响请求过程
     * abel.zhan 2023-05-30 用户中心增加了 通用资源缓存的代码，网关从hash中取得所有的通用资源
     *
     * @return 该资源是用户中心初始化存储的，用于鉴别么有纳入权限体系的资源
     * @author xiehl
     */
    private List<Resource> getCommonResource() {
        List<Resource> commonResources = new ArrayList<>();
        try {
            //status=2当做缓存的key
            Object o = redisTemplate.opsForValue().get(String.valueOf(2));
            Collection collection = redisTemplate.opsForHash().entries(Global.HASH_COMMON_RESOURCE_KEY).values();
            //获取全部的通用资源
            if (!CollectionUtil.isEmpty(collection)) {
                commonResources = JSONObject.parseArray(String.valueOf(collection), Resource.class);
            }
        } catch (Exception e) {
            log.error("通用资源缓存获取失败");
            throw new RuntimeException("通用资源缓存获取失败", e);
        }
        if (CollectionUtils.isEmpty(commonResources)) {
            commonResources = resourceMapper.commonResource();
        }
        return commonResources;
    }

    @Override
    public Integer roleAuthorization(String token, String api) {
        Assert.hasText(token, "token为空 roleAuthorization 参数异常");
        Assert.hasText(api, "api为空 roleAuthorization 参数异常");

        //解析token的 payload部分
        JSONObject joPayload = parseTokenPayload(token);
        // token 过期判断
        int tokenExpireResult = renewTokenService.expire(Long.parseLong(String.valueOf(joPayload.get("userId"))), token);
        if (BusinessResultConsts.TOKEN_EXPIRE_TURE == tokenExpireResult) {
            return BusinessResultConsts.AUTHOR_RESULT_TOKEN_EXPIRE;
        }
        if (BusinessResultConsts.TOKEN_EXPIRE_NEARLY == tokenExpireResult) {
            return BusinessResultConsts.AUTHOR_RESULT_TOKEN_NEARLY;
        }

        // 如果token没有过期则根据角色获取资源列表
        List<Long> roleIdList = JSONObject.parseArray(String.valueOf(joPayload.get("roleIds")), Long.class);

        List<Resource> resourceList = getResourcesByRoleIds(roleIdList);
        //提取验证使用的url
        return matchRequestUrlWithResources(getMatchURL(api, AUTHOR_REQUEST_URL_EXPR), resourceList)
                ? BusinessResultConsts.AUTHOR_RESULT_TRUE
                : BusinessResultConsts.AUTHOR_RESULT_MATCH_RESOURCE_FALSE;
    }

    @Override
    public List<Resource> getResourcesByRoleIds(List<Long> roleIds) {
        List<Resource> resourceList = new ArrayList<>();
        // 优先从 redis   db  和 usercentre
        resourceList = getRoleResourceFromRedis(roleIds);
        if (!CollectionUtils.isEmpty(resourceList)) {
            return resourceList;
        }
        resourceList = getRoleResourceFromUserCenter(roleIds);
        if (!CollectionUtils.isEmpty(resourceList)) {
            return resourceList;
        }
        return getRoleResourceFromDB(roleIds);
    }

    /**
     * 优先从redis取得资源 资源的初始化在用户中心完成
     *
     * @param roleIds 从token解析的角色id列表
     * @return 角色所对应的资源列表
     */
    private List<Resource> getRoleResourceFromRedis(List<Long> roleIds) {
        Assert.notEmpty(roleIds, "roleIdsList不能为空");
        Assert.notEmpty(roleIds, "roleIds不能为空");
        /**
         * 存储多个角色的资源综合的 Map<Long, RoleResource>参考如下使用，keylong为role_resource表的行id
         *  我们要的数据是 RoleResource
         */

        Set<Map<Long, RoleResource>> mapSetRoleResource = new HashSet<>();
        // 返回一个角色对应的资源map并且添加到set，例如两个角色这个 这个set就2个元素
        roleIds.stream().forEach(id -> {
            String hashKey = Global.HASH_ROLE_ID_KEY + id;
            // TODO 返回map的size为 0 ，待研究中
            Map roleResourceMap = redisTemplate.opsForHash().entries(hashKey);
            mapSetRoleResource.add(roleResourceMap);
        });
        /**
         * 找出每个角色对应的资源列表的具体的资源信息 使用set是为了避免重复
         * 首先遍历set中每个角色，取得map 然后遍历map将其中的resource取得增加到 resourceSet
         */

        Set<Resource> resourceSet = new HashSet<>();
        mapSetRoleResource.stream().forEach(map -> {
            Collection collection = map.values();
            for (Object roleResource : map.values()) {
                // 如果缓存和数据库都没取到该返回值为 new Resource
                String roleResourceJsonStr = JSONObject.toJSONString(roleResource);
                try {
                    RoleResource roleResourceTarget = JSONObject.parseObject(roleResourceJsonStr, RoleResource.class);
                    Resource resource = getResourceById(roleResourceTarget.getResourceId());
                    resourceSet.add(resource);
                } catch (Exception ex) {
                    log.error("JSONObject.parseObject 发生错误:" + ex.getMessage(), ex);
                }


            }
        });
        return new ArrayList<Resource>(resourceSet);
    }

    /**
     * 获取角色列表对应的资源不重复
     *
     * @param roleIds 角色列表 一个用户可能多个角色
     * @return 资源列表
     */
    private List<Resource> getRoleResourceFromDB(List<Long> roleIds) {
        Example example = new Example(RoleResource.class);
        example.createCriteria().andIn("roleId", roleIds);
        // 查询角色所包含的资源id集合
        List<RoleResource> roleResourceList = roleResourceMapper.selectByExample(example);
        //将资源ｉｄ集合转为resource对象集合
        List<Resource> resourceList = new ArrayList<>();
        roleResourceList.stream().forEach(roleResource -> {
            resourceList.add(getResourceById(roleResource.getResourceId()));
        });
        return resourceList;
    }

    /**
     * 通过feign-client调用用户中心接口
     *
     * @param roleIds
     * @return
     */
    private List<Resource> getRoleResourceFromUserCenter(List<Long> roleIds) {
        List<ResourceVO> resourceVOList = userService.getRoleResourceVO((Long[]) roleIds.toArray());
        return BeanUtil.copyList(resourceVOList, Resource.class);
    }

    /**
     * 通过 resource_id 的值得找到 Resource信息
     * 优先从缓存取得 如果没有则取数据库 如果再没有则取用户中心接口
     *
     * @param id resource 记录的 id
     * @return resource对象
     */
    private Resource getResourceById(Long id) {
        // 从 缓存获取 Resource 如果没有则从数据库获取 如果还没有则从接口获取
        Object resource = redisTemplate.opsForValue()
                .get(Global.RESOURCE_ID_KEY + id);
        if (resource != null) {
            return JSONObject.parseObject(JSONObject.toJSONString(resource), Resource.class);
        }
        // 取数据库 取用户中心接口
        resource = resourceMapper.selectByPrimaryKey(id);
        //TODO 如果空则调用用户中心接口 这里未实现请对应用户中心接口
        return resource == null ? null : new Resource();
    }

    @Override
    public boolean matchRequestUrlWithResources(String url, List<Resource> resources) {
        for (Resource resource : resources) {
            if (!StringUtils.isEmpty(resource.getApi())) {
                final Pattern pattern = compile(String.format("%s$", resource.getApi()));
                final Matcher matcherApi = pattern.matcher(url);
                // 匹配成功
                if (matcherApi.find()) {
                    return Boolean.TRUE;
                }
            }
        }
        // 没有匹配到则返回false
        return Boolean.FALSE;
    }

    /**
     * 根据正则表达式匹配URL
     *
     * @param url URL
     * @param reg 正则表达式
     * @return 匹配后的URL 解析了请求的字符串
     */
    private String getMatchURL(String url, String reg) {
        Assert.hasText(url, "url NOT NULL");
        Assert.hasText(reg, "url NOT NULL");
        String newUrl = "";
        final Pattern compile = compile(reg);
        Matcher matcher = compile.matcher(url);
        if (matcher.find()) {
            newUrl = matcher.group(1);
        }
        return newUrl;
    }

}
